Consuming AJAX Web Services
A referenced
ASP.NET AJAX Web service is exposed to the JavaScript code as a class
with the same name as the server class, including namespace information.
As we’ll see in a moment, the proxy class is a singleton and exposes
static methods for you to call. No instantiation is required,
which saves time and makes the call trigger more quickly. Let’s take a
look at the JavaScript proxy class generated from the public interface
of an AJAX Web service.
The Proxy Class
To understand the
structure of a JavaScript proxy class, we’ll consider what the ASP.NET
AJAX runtime generates for the aforementioned timeservice.asmx Web service. In the following example, the full name of the Web service class is Core35.WebServices.TimeService,
and therefore it is the name of the JavaScript proxy as well. Here’s
the first excerpt from the script injected into the client page for the
time Web service:
Type.registerNamespace('Core35.WebServices');
Core35.WebServices.TimeService = function()
{
Core35.WebServices.TimeService.initializeBase(this);
this._timeout = 0;
this._userContext = null;
this._succeeded = null;
this._failed = null;
}
Core35.WebServices.TimeService.prototype =
{
GetTime : function(succeededCallback, failedCallback, userContext)
{
return this._invoke(Core35.WebServices.TimeService.get_path(),
'GetTime', false, {}, succeededCallback,
failedCallback, userContext);
},
GetTimeFormat : function(timeFormat, succeededCallback,
failedCallback, userContext)
{
return this._invoke(Core35.WebServices.TimeService.get_path(),
'GetTimeAsFormat', false, {format:timeFormat},
succeededCallback, failedCallback, userContext);
}
}
Core35.WebServices.TimeService.registerClass(
Core35.WebServices.TimeService',
Sys.Net.WebServiceProxy);
Core35.WebServices.TimeService._staticInstance = new Core35.WebServices.TimeService();
As you can see from the prototype, the TimeService class has two methods—GetTime and GetTimeFormat—the
same two methods defined as Web methods in the server-side Web service
class. Both methods have an extended signature that encompasses
additional parameters other than the standard set of input arguments (as
defined by the server-side methods). In particular, you see two
callbacks to call—one for the success of the call, and one for
failure—and an object that represents the context of the call.
Internally, each method on the proxy class yields to a private member of
the parent class—Sys.Net.WebServiceProxy—that uses XMLHttpRequest to physically send bytes to the server.
The
last statement in the preceding code snippet creates a global instance
of the proxy class. The methods you invoke from within your JavaScript
to execute remote calls are defined around this global instance, as
shown here:
Core35.WebServices.TimeService.GetTime = function(
onSuccess,onFailed,userContext)
{
Core35.WebServices.TimeService._staticInstance.GetTime(
onSuccess, onFailed, userContext);
}
Core35.WebServices.TimeService.GetTimeFormat = function(
format, onSuccess, onFailed, userContext)
{
Core35.WebServices.TimeService._staticInstance.GetTimeFormat(
format, onSuccess, onFailed, userContext);
}
The definition of the proxy class is completed with a few public properties, as described in Table 2.
Table 2. Static Properties on a JavaScript Proxy Class
Property | Description |
---|
path | Gets and sets the URL of the underlying Web service |
timeout | Gets and sets the duration (in seconds) before the method call times out |
defaultSucceededCallback | Gets and sets the default JavaScript callback function to invoke for a successful call |
defaultFailedCallback | Gets and sets the default JavaScript callback function, if any, to invoke for a failed or timed-out call |
defaultUserContext | Gets and sets the default JavaScript object, if any, to be passed to success and failure callbacks |
If you set a “default
succeeded” callback, you don’t have to specify a “succeeded” callback in
any successive call as long as the desired callback function is the
same. The same holds true for the failed callback and the user context
object. The user context object is any JavaScript object, filled with
any information that makes sense to you, that gets automatically passed
to any callback that handles the success or failure of the call.
Note
The JavaScript code injected for the proxy class uses the path
property to define the URL to the Web service. You can change the
property programmatically to redirect the proxy to a different URL. |
Executing Remote Calls
A
Web service call is an operation that the page executes in response to a
user action such as a button click. Here’s the typical way of attaching
some JavaScript to a client button click:
<input type="button" value="Get Time" onclick="getTime()" />
The button, preferably, is a client button, but it can also be a classic server-side Button object submit button as long as it sets the OnClientClick property to a piece of JavaScript code that returns false to prevent the alternative default submit action:
<asp:Button ID="Button1" runat="server" Text="Button"
OnClientClick="getTime();return false;" />
The getTime
function collects any required input data and then calls the desired
static method on the proxy class. If you plan to assign default values
to callbacks or the user context object, the best place to do it is in
the pageLoad function.
<script language="javascript" type="text/javascript">
function pageLoad()
{
Core35.WebServices.TimeService.set_defaultFailedCallback(methodFailed);
}
function getTime()
{
Core35.WebServices.TimeService.GetTimeFormat(
"ddd, dd MMMM yyyy [hh:mm:ss]", methodComplete);
}
function methodComplete(results, context, methodName)
{
$get("Label1").innerHTML = results;
}
function methodFailed(errorInfo, context, methodName)
{
$get("Label1").innerHTML = String.format(
"Execution of method '{0}' failed because of the
following:\r\n'{1}'",
methodName, errorInfo.get_message());
}
</script>
Because the Web
service call proceeds asynchronously, you need callbacks to catch up
both in the case of success and failure. The signature of the callbacks
is similar, but the internal format of the results parameter can change
quite a bit:
function method(results, context, methodName)
Table 3 provides more details about the various arguments.
Table 3. Arguments for JavaScript Web Service Callback Functions
Argument | Description |
---|
results | Indicates the return value from the method in the case of success. In the case of failure, a JavaScript Error object mimics the exception that occurred on the server during the execution of the method. |
context | The user context object passed to the callback. |
methodName | The name of the Web service method that was invoked. |
Based on the previous code, if the call is successful the methodCompleted callback is invoked to update the page. The result is shown in Figure 2.
Error Handling
The “failed” callback
kicks in when an exception occurs on the server during the execution of
the remote method. In this case, the HTTP response contains an HTTP 500
error code (internal error) and the body of the response looks like the
following:
{"message":"Exception thrown for testing purposes",
"stackTrace":" at Core35.WebServices.MyDataService.Throw() in
d:\\Core35\\App_Code\\Services\\MyDataService.cs:line
62","ExceptionType":"System.InvalidOperationException"}
On the client, the server exception is exposed through a JavaScript Error object dynamically built based on the message and a stack trace received from the server. This Error object is exposed to the “failed” callback via the results argument. You can read back the message and stack trace through message and stackTrace properties on the Error object.
You
can use a different error handler callback for each remote call, or you
can designate a default function to be invoked if one is not otherwise
specified. However, ASP.NET AJAX still defines its own default callback,
which is invoked when it gets no further information from the client
developer. The system-provided error handler callback simply pops up a
message box with the message associated with the server exception. If
you define your own “failed” callback, you can avoid message boxes and
incorporate any error message directly in the body of the page.
Giving User Feedback
A remote call might take a
while to complete because the operation to execute is fairly heavy or
just because of the network latency. In any case, you might feel the
need to show some feedback to the user to let her know that the system
is still working. We saw that the Microsoft
AJAX library has a built-in support for an intermediate progress screen
and also a client-side eventing model. Unfortunately, this functionality
is limited to calls that originate within updatable panels. For classic
remote method calls, you have to personally take care of any user
feedback.
You bring up the wait message, the animated GIF, or whatever else you need just before you call the remote method:
function takeaWhile()
{
// In this example, the Feedback element is a <span> tag
$get("Feedback").innerHTML = "Please, wait ...";
Core35.WebServices.MySampleService.VeryLengthyTask(
methodCompletedWithFeedback, methodFailedWithFeedback);
}
In the “completed” callback, you reset the user interface first and then proceed:
function methodCompletedWithFeedback(results, context, methodName)
{
$get("Feedback").innerHTML = "";
...
}
Note that you should
also clear the user interface in the case of errors. In addition to
showing some sort of wait message to the user, you should also consider
that other elements in the page might need to be disabled during the
call. If this is the case, you need to disable them before the call and
restore them later.
Handling Timeouts
A remote call that takes a
while to complete is not necessarily a good thing for the application.
Keep in mind that calls that work asynchronously for the client are not
necessarily asynchronous for the ASP.NET runtime. In particular, note
that when you make a client call to an .asmx Web service, you are invoking the .asmx
directly. For this request, only a synchronous handler is available in
the ASP.NET runtime. This means that regardless of how the client
perceives the ongoing call, an ASP.NET thread is entirely blocked
(waiting for results) until the method is done. To mitigate the impact
of lengthy AJAX methods on the application scalability, you can set a
timeout:
Core35.WebServices.MySampleService.set_timeout(3000);
The timeout
attribute is global and applies to all methods of the proxy class. This
means that if you want to time out only one method call, you have to
reset the timeout for all calls you’re making from the page. To reset
the timeout, you just set the timeout property to zero:
Core35.WebServices.MySampleService.set_timeout(0);
When the request times
out, there’s no response received from the server. It’s simply a call
that is aborted from the client. After all, you can’t control what’s
going on with the server. The best you can do is abort the request on
the client and take other appropriate measures, such as having the user
try again later.
Considerations for AJAX-Enabled Web Services
Now that we know how to
tackle AJAX-enabled Web services, it would be nice to spend some time
reflecting on some other aspects of them—for example, why use local
services?
Why Local Web Services?
To make sure you
handle AJAX Web services the right way, think of them as just one
possible way of exposing a server API to a JavaScript client. You focus
on the interface that must be exposed and then choose between ASP.NET
Web services, WCF services, and page methods for its actual
implementation. Looking at it from this angle, you might find it to be
quite natural that the Web service has to be hosted in the same ASP.NET
AJAX application that is calling it.
But why can’t you
just call into any SOAP-based Web services out there? There are two main
reasons: security and required support for JSON serialization.
For security reasons,
browsers tend to stop script-led cross-site calls. (Not all scripts are
benevolent.) Most browsers bind scripted requests to what is often
referred to as the “same origin policy.” Defined, it claims that no
documents can be requested via script that have a different port,
server, or protocol than the current page. In light of this, you can use
the XMLHttpRequest object to place asynchronous calls as long as your request hits the same server endpoint that served the current page.
Because of the cross-site limitations of XMLHttpRequest
in most browsers, ASP.NET doesn’t allow you to directly invoke a Web
service that lives on another IIS server or site. Without this limitation,
nothing would prevent you from invoking a Web service that is resident
on any platform and Web server environment, but then your users are
subject to potential security threats from less scrupulous applications.
With this limitation in place, though, an additional issue shows up:
the inability of your host ASP.NET AJAX environment to build a
JavaScript proxy class for the remote, non–ASP.NET AJAX Web service.
Note
Because
of the impact that blocked cross-site calls have on general AJAX
development, a new standard might emerge in the near future to enable
such calls from the browser. It might be desirable that the client sends
the request and dictates the invoked server accept or deny cross-site
calls made via XMLHttpRequest.
As of this writing, though, the possibility of direct cross-site calls
from AJAX clients (not just ASP.NET AJAX Extensions) remains limited to
the use of IFRAMEs and finds no built-in support in ASP.NET 3.5. |
Why JSON-Based Web Services?
A call to a Web
service hosted by the local ASP.NET AJAX application is not conducted
using SOAP as you might expect. SOAP is XML-based, and parsing XML on
the client is very expensive in terms of memory and processing
resources. It means that an XML parser must be available in JavaScript,
and an XML parser is never an easy toy to build and manage, especially
using a relatively lightweight tool such as JavaScript. So a different
format is required to pack messages to be sent and unpack messages just
received. Like SOAP and XML schemas together, though, this new format
must be able to serialize an object’s public properties and fields to a
serial text-based format for transport. The format employed by ASP.NET
AJAX Web services is JSON.
The client-side
ASP.NET AJAX network stack takes care of creating JSON strings for each
parameter to pass remotely. The JavaScript class that does that is
called Sys.Serialization.JavaScriptSerializer.
On the server, ad hoc formatter classes receive the data and use .NET
reflection to populate matching managed classes. On the way back, .NET
managed objects are serialized to JSON strings and sent over. The script
manager is called to guarantee that proper classes referenced in the
JSON strings—the Web service proxy class—exist on the client.
Runtime Support for JSON-Based Web Services
As a developer, you
don’t necessarily need to know much about the JSON format. You normally
don’t get close enough to the heart of the system to directly manage
JSON strings. However, a JSON string represents an object according to
the following sample schema:
{
"__type":"IntroAjax.Customer",
"ID":"ANATR",
"ContactName":"Ana Trujillo"
...
}
You’ll
find a number of comma-separated tokens wrapped in curly brackets. Each
token is, in turn, a colon-separated string. The left part, in quotes,
represents the name of the property; the right part, in quotes,
represents the serialized version of the property value. If the property
value is not a primitive type, it gets recursively serialized via JSON.
If the object is an instance of a known type (that is, it is not an
untyped JavaScript associative array), the class name is inserted as the
first piece of information associated with the __type
property. Any information being exchanged between an ASP.NET AJAX
client and an ASP.NET AJAX Web service is serialized to the JSON format.
To the actual Web
service, the transport format is totally transparent—be it SOAP, JSON,
plain-old XML (POX), or whatever else. The runtime infrastructure takes
care of deserializing the content of the message and transforms it into
valid input for the service method. The ASP.NET AJAX runtime recognizes a
call directed to an AJAX Web service because of the particular value of
the Content-Type request header. Here’s an excerpt from the Microsoft AJAX client library where the header is set:
request.get_headers()['Content-Type'] = 'application/json; charset=utf-8';
The value of this
header is used to filter incoming requests and direct them to the
standard ASP.NET XML Web service HTTP handler or to the made-to-measure
ASP.NET AJAX Web service handler that will do all the work with the JSON
string.